using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace Dida.Test.ParamEditor2D
{
	public class ParamEditor2DModel
	{
		HashSet<ControlPoint2D> controlPoints = new HashSet<ControlPoint2D>();
		HashSet<ControlPoint2D> selection = new HashSet<ControlPoint2D>();

		BroadcastingSet<ControlPoint2D> controlPointsWrapper;

		public ParamEditor2DModel()
		{
			controlPointsWrapper = new BroadcastingSet<ControlPoint2D>(controlPoints);
			controlPointsWrapper.ItemAdded += OnControlPointAdded;
		}

		private void OnControlPointAdded(object sender, 
			BroadcastingSet<ControlPoint2D>.ItemAddedArgs args)
		{
			args.Value.SelectedChanged += OnCPSelectionChanged;
		}

		public void SetSelection(ControlPoint2D selectThis)
		{
			Debug.Assert(controlPoints.Contains(selectThis));

			selection.Clear();
			selection.Add(selectThis);

			foreach(ControlPoint2D cp in controlPoints)
				cp.Selected = cp == selectThis;
		}

		public void SetSelection(ISet<ControlPoint2D> selectThis)
		{
			selection = new HashSet<ControlPoint2D>(selectThis);

			HashSet<ControlPoint2D> unselected = selection.GetDifference(selectThis);
			foreach (ControlPoint2D cp in unselected)
				cp.Selected = false;

			foreach (ControlPoint2D cp in selectThis)
			{
				Debug.Assert(controlPoints.Contains(cp));

				cp.Selected = true;
			}
		}

		public void ClearSelection()
		{
			foreach (ControlPoint2D cp in selection)
				cp.Selected = false;
			selection.Clear();
		}

		public ISet<ControlPoint2D> ControlPoints
		{
			get { return controlPointsWrapper; }
		}

		private void OnCPSelectionChanged(object sender,EventArgs args)
		{
			ControlPoint2D cp = (ControlPoint2D)sender;
			if (!cp.Selected)
			{
				Debug.Assert(selection.Contains(cp));
				selection.Remove(cp);
			}
			else
			{
				Debug.Assert(!selection.Contains(cp));
				selection.Add(cp);
			}

			if (CPSelectionChanged != null)
			{
				CPSelectionChangedArgs refireArgs = new CPSelectionChangedArgs();
				refireArgs.controlPoint = (ControlPoint2D)sender;
				CPSelectionChanged(this, refireArgs);
			}
		}

		public class CPSelectionChangedArgs : EventArgs
		{
			public ControlPoint2D controlPoint;
		}
		public delegate void CPSelectionChangedHandler(object sender, CPSelectionChangedArgs args);
		public event CPSelectionChangedHandler CPSelectionChanged;
	}

	public abstract class ParamEditor2DController
	{
		internal ParamEditor2DView view;
		internal ParamEditor2DModel model;

		protected ParamEditor2DView View
		{
			get 
			{
				Debug.Assert(model != null, "No view set. Are you using the control from inside a ParamEditor2DView?");
				return view; 
			}
		}

		protected ParamEditor2DModel Model
		{
			get 
			{
				Debug.Assert(model != null, "No model set. Are you using the control from inside a ParamEditor2DView?");
				return model; 
			}
		}

		public virtual void Draw(Gdk.Drawable dr) { }
		public virtual void OnButtonPress(Gdk.Point ctrlPos, Vector2D logicalPos) { }
		public virtual void OnButtonRelease(Gdk.Point ctrlPos, Vector2D logicalPos) { }
		public virtual void OnMouseMotion(Gdk.Point ctrlPos, Vector2D logicalPos) { }
	}

	public class ParamEditor2DView : Gtk.EventBox
	{
		Gdk.GC bgContext;
		Gdk.GC linesGC;
		Gdk.GC selectedPtGC;
		Gdk.GC unselectedPtGC;

		float zoom = 1;
		Vector2D origin = new Vector2D(0,0);
		int defGridPixelSize = 16;

		ParamEditor2DModel model = new ParamEditor2DModel();
		Stack<ParamEditor2DController> controllersStack = new Stack<ParamEditor2DController>();

		public ParamEditor2DView()
		{
			model.ControlPoints.Add(new ControlPoint2D(new Vector2D(20, 20)));
			model.CPSelectionChanged += OnCPSelectionChanged;

			SelectController selectController = new SelectController();
			selectController.view = this;
			selectController.model = model;
			controllersStack.Push(selectController);
		}

		public ParamEditor2DModel Model
		{
			get { return model; }
		}

		private void OnCPSelectionChanged(object sender, ParamEditor2DModel.CPSelectionChangedArgs args)
		{
			Gdk.Rectangle cpRect = args.controlPoint.GetRectInView(this);
			QueueDrawArea(cpRect.X, cpRect.Y, cpRect.Width + 1, cpRect.Height + 1);
		}

		protected override void OnRealized()
		{
			base.OnRealized();
			
			bgContext = new Gdk.GC(GdkWindow);
			bgContext.RgbFgColor = new Gdk.Color(128, 128, 128);
			bgContext.RgbBgColor = new Gdk.Color(128, 128, 128);

			linesGC = new Gdk.GC(GdkWindow);
			linesGC.RgbFgColor = new Gdk.Color(96, 96, 96);
			linesGC.SetLineAttributes(1, Gdk.LineStyle.OnOffDash, 
				Gdk.CapStyle.Butt, Gdk.JoinStyle.Miter);
			linesGC.SetDashes(0, new sbyte[] { 1, 1 }, 2);

			selectedPtGC = new Gdk.GC(GdkWindow);
			selectedPtGC.RgbFgColor = new Gdk.Color(255, 255, 0);

			unselectedPtGC = new Gdk.GC(GdkWindow);
			unselectedPtGC.RgbFgColor = new Gdk.Color(192, 192, 192);
		}

		public Gdk.Point TransformPtLogicalToCtrl(Vector2D pt)
		{
			return new Gdk.Point(
				(int)(pt.x * zoom + origin.x),
				(int)(pt.y * zoom + origin.y));
		}

		public Vector2D TransformPtCtrlToLogical(Gdk.Point pt)
		{
			return new Vector2D(
				(pt.X - origin.x) / zoom,
				(pt.Y - origin.y) / zoom);
		}

		public Gdk.Rectangle GetControlPointRect(ControlPoint2D cp)
		{
			Gdk.Point center = TransformPtLogicalToCtrl(cp.Position);
			Gdk.Rectangle rect = new Gdk.Rectangle(center.X - 2, center.Y - 2, 4, 4);
			return rect;
		}

		protected override bool OnExposeEvent(Gdk.EventExpose evnt)
		{
			base.OnExposeEvent(evnt);

			Gdk.Drawable dr = GdkWindow;

			dr.DrawRectangle(bgContext, true, Allocation);

			int gridPow = (int)Math.Log(zoom,2);
			int gridSize = defGridPixelSize << gridPow;

			for(int x = Allocation.X;x < Allocation.Width;x += gridSize)
				dr.DrawLine(linesGC,x,0,x,Allocation.Height);
			for (int y = Allocation.Y; y < Allocation.Height; y += gridSize)
				dr.DrawLine(linesGC, 0, y, Allocation.Width, y);

			foreach (ControlPoint2D cp in model.ControlPoints)
			{
				Gdk.GC gc = cp.Selected ? selectedPtGC : unselectedPtGC;
				Gdk.Rectangle rect = GetControlPointRect(cp);
				dr.DrawRectangle(gc, false, rect);
			}

			if (controllersStack.Count != 0)
			{
				controllersStack.Peek().Draw(dr);
			}
			
			return true;
		}

		protected override bool OnButtonPressEvent(Gdk.EventButton evnt)
		{
			if (controllersStack.Count != 0)
			{
				Gdk.Point ctrlPos = new Gdk.Point((int)evnt.X, (int)evnt.Y);
				Vector2D logicalPos = new Vector2D();
				controllersStack.Peek().OnButtonPress(ctrlPos,logicalPos);
			}
			
			return true;
		}

		protected override bool OnButtonReleaseEvent(Gdk.EventButton evnt)
		{
			if (controllersStack.Count != 0)
			{
				Gdk.Point ctrlPos = new Gdk.Point((int)evnt.X, (int)evnt.Y);
				Vector2D logicalPos = new Vector2D();
				controllersStack.Peek().OnButtonRelease(ctrlPos, logicalPos);
			}

			return true;
		}

		protected override bool OnMotionNotifyEvent(Gdk.EventMotion evnt)
		{
			if (controllersStack.Peek() != null)
			{
				Gdk.Point ctrlPos = new Gdk.Point((int)evnt.X, (int)evnt.Y);
				Vector2D logicalPos = new Vector2D();
				controllersStack.Peek().OnMouseMotion(ctrlPos, logicalPos);
			}

			return true;
		}
	}

	public class ControlPoint2D
	{
		Vector2D pos;
		bool selected = false;

		public ControlPoint2D(Vector2D pos)
		{
			this.pos = pos;
		}

		public bool Selected
		{
			get { return selected; }
			set
			{
				if (selected == value)
					return;

				selected = value;

				if (SelectedChanged != null)
					SelectedChanged(this, EventArgs.Empty);
			}
		}

		public event EventHandler SelectedChanged;

		public Vector2D Position
		{
			get { return pos; }
			set
			{
				pos = value;

				if (PositionChanged != null)
					PositionChanged(this, EventArgs.Empty);
			}
		}

		public event EventHandler PositionChanged;

		/// <summary>
		/// Gets the rectangle that is used to visualize this control point.
		/// The rectangle that is returned is in pixels, and is relative to
		/// the top left corner of the given <see cref = "ParamEditor2DVIew"/>
		/// </summary>
		/// <param name="view">The view in which the control point exists.</param>
		/// <returns>The rectangle of this control point</returns>
		/// <remarks>
		/// This basically is a utility function that forewards the call to 
		/// <see cref = "ParamEditor2DView.GetControlPointRect"/>. Having
		/// the function in this class may seem more intuitive.
		/// </remarks>
		public Gdk.Rectangle GetRectInView(ParamEditor2DView view)
		{
			return view.GetControlPointRect(this);
		}
	}
}